/*******************************************************************************
 * Copyright (c) 2005, 2010 Intel Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * Intel Corporation - Initial API and implementation
 *******************************************************************************/

package org.eclipse.cdt.managedbuilder.internal.core;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import org.eclipse.cdt.managedbuilder.core.IConfiguration;
import org.eclipse.cdt.managedbuilder.core.IManagedBuildInfo;
import org.eclipse.cdt.managedbuilder.core.IManagedOptionValueHandler;
import org.eclipse.cdt.managedbuilder.core.IManagedProject;
import org.eclipse.cdt.managedbuilder.core.IResourceConfiguration;
import org.eclipse.cdt.managedbuilder.core.ManagedBuildManager;
import org.eclipse.cdt.managedbuilder.core.ManagedBuilderCorePlugin;
import org.eclipse.cdt.managedbuilder.core.ManagedCProjectNature;
import org.eclipse.cdt.managedbuilder.makegen.IManagedBuilderMakefileGenerator;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IResourceRuleFactory;
import org.eclipse.core.resources.ISaveContext;
import org.eclipse.core.resources.ISaveParticipant;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.MultiRule;

public class ResourceChangeHandler implements IResourceChangeListener, ISaveParticipant {

	private Map<IProject, IManagedBuildInfo> fRmProjectToBuildInfoMap = new HashMap<IProject, IManagedBuildInfo>();

	private class ResourceConfigurationChecker implements IResourceDeltaVisitor{
		private IResourceDelta fRootDelta;
		private HashMap<IProject, IManagedBuilderMakefileGenerator> fBuildFileGeneratorMap = new HashMap<IProject, IManagedBuilderMakefileGenerator>();
		private HashSet<IPath> fValidatedFilesSet = new HashSet<IPath>();
		private HashSet<IProject> fModifiedProjects = new HashSet<IProject>();

		public ResourceConfigurationChecker(IResourceDelta rootDelta){
			fRootDelta = rootDelta;
		}

		public IProject[] getModifiedProjects(){
			return fModifiedProjects.toArray(new IProject[fModifiedProjects.size()]);
		}

		@Override
		public boolean visit(IResourceDelta delta) throws CoreException {
			IResource dResource = delta.getResource();
			int rcType = dResource.getType();

			if(rcType == IResource.PROJECT || rcType == IResource.FOLDER){
				IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
				IProject project = null;
				IResource rcToCheck = null;
				switch (delta.getKind()) {
				case IResourceDelta.REMOVED :
					if (rcType == IResource.PROJECT){
						IManagedBuildInfo info = fRmProjectToBuildInfoMap.remove(dResource);

						if((delta.getFlags() & IResourceDelta.MOVED_TO) == 0) {
							if(info != null){
								sendClose(info);
								PropertyManager.getInstance().clearProperties(info.getManagedProject());
							}
							break;
						}
					}
				case IResourceDelta.CHANGED :
					if ((delta.getFlags() & IResourceDelta.MOVED_TO) != 0) {
						IPath path = delta.getMovedToPath();
						if(path != null){
							project = root.findMember(path.segment(0)).getProject();
							if(project != null && rcType == IResource.FOLDER)
								rcToCheck = root.getFolder(substituteProject(dResource.getFullPath(),project.getName()));
						}
						break;
					}
				default:
					project = dResource.getProject();
					if(rcType == IResource.FOLDER)
						rcToCheck = dResource;
					break;
				}

				if(project != null) {
					IManagedBuilderMakefileGenerator makeGen = getInitializedGenerator(project);
					if(makeGen != null){
						if(rcToCheck == null || !makeGen.isGeneratedResource(rcToCheck))
							return true;
					}
				}
				return false;
			} else if (rcType == IResource.FILE && !dResource.isDerived()) {
				int flags = delta.getFlags();
				switch (delta.getKind()) {
				case IResourceDelta.REMOVED :
					if ((flags & IResourceDelta.MOVED_TO) == 0) {
						handleDeleteFile(dResource.getFullPath());
						break;
					}
				case IResourceDelta.ADDED :
				case IResourceDelta.CHANGED :
				    if ((flags & IResourceDelta.MOVED_TO) != 0) {
				    	IPath path = delta.getMovedToPath();
				    	if (path != null) {
				    		handleRenamedFile(
				    				  dResource.getFullPath(),
				    				  path);
				    	}
				    } else if ((flags & IResourceDelta.MOVED_FROM) != 0) {
						IPath path = delta.getMovedFromPath();
				    	if (path != null) {
				    		handleRenamedFile(
				    				  path,
				    				  dResource.getFullPath());
				    	}
				    }
					break;

				default:
					break;
				}
				return false;
			}
			return true;	//  visit the children
		}

		private IPath substituteProject(IPath path, String projectName){
			return new Path(projectName).makeAbsolute().append(path.removeFirstSegments(1));
		}

		private void handleRenamedFile(IPath fromPath, IPath toPath){
			if(!fValidatedFilesSet.add(fromPath))
				return;

			IProject fromProject = findModifiedProject(fromPath.segment(0));
			if(fromProject == null)
				return;
			IManagedBuilderMakefileGenerator fromMakeGen = getInitializedGenerator(fromProject);
			IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
			if(fromMakeGen == null || fromMakeGen.isGeneratedResource(root.getFile(substituteProject(fromPath,fromProject.getName()))))
				return;

			IManagedBuildInfo fromInfo = ManagedBuildManager.getBuildInfo(fromProject);

			IProject toProject = root.findMember(toPath.uptoSegment(1)).getProject();
			IManagedBuildInfo toInfo = toProject != null ?
					ManagedBuildManager.getBuildInfo(toProject) :
						null;
			IManagedBuilderMakefileGenerator toMakeGen = toProject != null ?
					getInitializedGenerator(toProject) :
						null;
			if(toMakeGen != null && toMakeGen.isGeneratedResource(root.getFile(toPath)))
				toInfo = null;

			if(fromInfo == toInfo){
				//the resource was moved within the project scope
				if(updateResourceConfigurations(fromInfo,fromPath,toPath) && toProject != null)
					fModifiedProjects.add(toProject);
			} else {
				if(fromInfo != null && toInfo != null){
					//TODO: this is the case when the resource
					//is moved from one managed project to another
					//should we handle this?
					//e.g. add resource configurations to the destination project?
				}
				if(fromInfo != null && removeResourceConfigurations(fromInfo,fromPath))
					fModifiedProjects.add(fromProject);
			}
		}

		private void handleDeleteFile(IPath path){
			IProject project = findModifiedProject(path.segment(0));
			if(project != null){
				IManagedBuildInfo info = ManagedBuildManager.getBuildInfo(project);
				if(info != null
						&& removeResourceConfigurations(info,path))
					fModifiedProjects.add(project);
			}
		}

		//finds the project geven the initial project name
		//That is:
		// if the project of a given name was renamed returns the renamed project
		// if the project of a given name was removed returns null
		// if the project of a given name was neither renamed or removed
		//   returns the project of that name or null if the project does not exist
		//
		private IProject findModifiedProject(final String oldProjectName){
			IResourceDelta projectDelta = fRootDelta.findMember(new Path(oldProjectName));
			boolean replaced = false;
			if(projectDelta != null) {
				switch(projectDelta.getKind()){
					case IResourceDelta.REMOVED :
					    if ((projectDelta.getFlags() & IResourceDelta.MOVED_TO) == 0) {
					    	return null;
					    }
					case IResourceDelta.CHANGED :
					    if ((projectDelta.getFlags() & IResourceDelta.MOVED_TO) != 0) {
					    	IPath path = projectDelta.getMovedToPath();
					    	if(path != null)
					    		return ResourcesPlugin.getWorkspace().getRoot().findMember(path).getProject();
					    }
					    break;
				}
			}

			final IProject project[] = new IProject[1];
			try {
				fRootDelta.accept(new IResourceDeltaVisitor() {
					@Override
					public boolean visit(IResourceDelta delta) throws CoreException {
						IResource dResource = delta.getResource();
						int rcType = dResource.getType();
						if(rcType == IResource.ROOT) {
							return true;
						} else if(rcType == IResource.PROJECT){
							switch(delta.getKind()){
							case IResourceDelta.ADDED :
							case IResourceDelta.CHANGED :
						      if ((delta.getFlags() & IResourceDelta.MOVED_FROM) != 0) {
						    	  IPath path = delta.getMovedFromPath();
						    	  if (path != null && path.segment(0).equals(oldProjectName)) {
						    		  project[0] = dResource.getProject();
						    	  }
						      }
							break;
							default:
								break;
							}
						}
						return false;
					}
				});
			} catch (CoreException e) {
			}

			if(project[0] == null && !replaced)
				project[0] = ResourcesPlugin.getWorkspace().getRoot().findMember(oldProjectName).getProject();
			return project[0];
		}

		private IManagedBuilderMakefileGenerator getInitializedGenerator(IProject project){
			IManagedBuilderMakefileGenerator makeGen = fBuildFileGeneratorMap.get(project);
			if (makeGen == null) {
				try {
					if (project.hasNature(ManagedCProjectNature.MNG_NATURE_ID)) {
						// Determine if we can access the build info before actually trying
						// If not, don't try, to avoid putting up a dialog box warning the user
						if (!ManagedBuildManager.canGetBuildInfo(project)) return null;

						IManagedBuildInfo buildInfo = ManagedBuildManager.getBuildInfo(project);
						if (buildInfo != null){
							IConfiguration defaultCfg = buildInfo.getDefaultConfiguration();
							if (defaultCfg != null) {
								makeGen = ManagedBuildManager.getBuildfileGenerator(defaultCfg);
								makeGen.initialize(project,buildInfo,new NullProgressMonitor());
								fBuildFileGeneratorMap.put(project,makeGen);
							}
						}
					}
				} catch (CoreException e){
					return null;
				}
			}
			return makeGen;
		}
	}

	public void sendClose(IProject project){
		sendClose(ManagedBuildManager.getBuildInfo(project,false));
	}

	private void sendClose(IManagedBuildInfo info){
		if(info != null){
			IManagedProject managedProj = info.getManagedProject();
			if (managedProj != null) {
				IConfiguration cfgs[] = managedProj.getConfigurations();

				for(int i = 0; i < cfgs.length; i++)
					ManagedBuildManager.performValueHandlerEvent(cfgs[i], IManagedOptionValueHandler.EVENT_CLOSE, true);
			}
		}
	}

	/*
	 *  I R e s o u r c e C h a n g e L i s t e n e r
	 */

	/* (non-Javadoc)
	 *
	 *  Handle the renaming and deletion of project resources
	 *  This is necessary in order to update ResourceConfigurations and AdditionalInputs
	 *
	 * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
	 */
	@Override
	public void resourceChanged(IResourceChangeEvent event) {
		if (event.getSource() instanceof IWorkspace) {

			switch (event.getType()) {
				case IResourceChangeEvent.PRE_CLOSE:
					IResource proj = event.getResource();
					if(proj instanceof IProject)
						sendClose((IProject)proj);
					break;
				case IResourceChangeEvent.PRE_DELETE :
					IResource rc = event.getResource();
					if(rc instanceof IProject){
						IProject project = (IProject)rc;
						try {
							if (project.hasNature(ManagedCProjectNature.MNG_NATURE_ID)) {
								IManagedBuildInfo info = ManagedBuildManager.getBuildInfo(project);
								if(info != null)
									fRmProjectToBuildInfoMap.put(project, info);
							}
						} catch (CoreException e) {
						}
					}
				case IResourceChangeEvent.POST_CHANGE :
				case IResourceChangeEvent.POST_BUILD :
					IResourceDelta resDelta = event.getDelta();
					if (resDelta == null) {
						break;
					}
					try {
						ResourceConfigurationChecker rcChecker = new ResourceConfigurationChecker(resDelta);
						resDelta.accept(rcChecker);

						//saving info for the modified projects
						initInfoSerialization(rcChecker.getModifiedProjects());

					} catch (CoreException e) {
						ManagedBuilderCorePlugin.log(e);
					}
					break;
				default :
					break;
			}
		}
	}

	private void initInfoSerialization(final IProject projects[]){
		if(projects.length == 0)
			return;
		IWorkspace workspace = ResourcesPlugin.getWorkspace();
		IResourceRuleFactory ruleFactory = workspace.getRuleFactory();
		ISchedulingRule buildInfoSaveRule;
		if(projects.length == 1){
			buildInfoSaveRule = ruleFactory.modifyRule(projects[0]);
		} else {
			ISchedulingRule rules[] = new ISchedulingRule[projects.length];
			for(int i = 0; i < rules.length; i++)
				rules[i] = ruleFactory.modifyRule(projects[i]);
			buildInfoSaveRule = MultiRule.combine(rules);
		}

		Job savingJob = new Job(ManagedMakeMessages.getResourceString("ResourceChangeHandler.buildInfoSerializationJob")){ 	//$NON-NLS-1$
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				for(int i = 0; i < projects.length; i++){
					ManagedBuildManager.saveBuildInfo(projects[i],true);
				}
				return new Status(
						IStatus.OK,
						ManagedBuilderCorePlugin.getUniqueIdentifier(),
						IStatus.OK,
						new String(),
						null);
			}
		};
		savingJob.setRule(buildInfoSaveRule);

		savingJob.schedule();
	}

	private boolean updateResourceConfigurations(IManagedBuildInfo info, IPath oldPath, IPath newPath){
		boolean changed = false;
		if(!oldPath.equals(newPath)){
			IManagedProject mngProj = info.getManagedProject();
			if(mngProj != null){
				IConfiguration configs[] = mngProj.getConfigurations();
				if(configs != null && configs.length > 0){
					for(int i = 0; i < configs.length; i++){
						if(updateResourceConfiguration(configs[i],oldPath,newPath))
							changed = true;
					}
				}
			}
		}
		return changed;
	}

	private boolean removeResourceConfigurations(IManagedBuildInfo info, IPath path){
		boolean changed = false;
		IManagedProject mngProj = info.getManagedProject();
		if(mngProj != null){
			IConfiguration configs[] = mngProj.getConfigurations();
			if(configs != null && configs.length > 0){
				for(int i = 0; i < configs.length; i++){
					if(removeResourceConfiguration(configs[i],path))
						changed = true;
				}
			}
		}
		return changed;
	}

	private boolean updateResourceConfiguration(IConfiguration config, IPath oldPath, IPath newPath){
		IResourceConfiguration rcCfg = config.getResourceConfiguration(oldPath.toString());
		if(rcCfg != null && !oldPath.equals(newPath)){
//			config.removeResourceConfiguration(rcCfg);
			rcCfg.setResourcePath(newPath.toString());
//			rcCfg.setRebuildState(true);
//			((Configuration)config).addResourceConfiguration((ResourceConfiguration)rcCfg);
//			config.setRebuildState(true);
			return true;
		}
		return false;
	}

	private boolean removeResourceConfiguration(IConfiguration config, IPath path){
		IResourceConfiguration rcCfg = config.getResourceConfiguration(path.toString());
		if(rcCfg != null){
			config.removeResourceConfiguration(rcCfg);
//			config.setRebuildState(true);
			return true;
		}
		return false;
	}

	/*
	 *  I S a v e P a r t i c i p a n t
	 */

	/* (non-Javadoc)
	 * @see org.eclipse.core.resources.ISaveParticipant#saving(org.eclipse.core.resources.ISaveContext)
	 */
	@Override
	public void saving(ISaveContext context) throws CoreException {
		PropertyManager.getInstance().serialize();

		//Request a resource delta to be used on next activation.
	    context.needDelta();
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.resources.ISaveParticipant#doneSaving(org.eclipse.core.resources.ISaveContext)
	 */
	@Override
	public void doneSaving(ISaveContext context) {
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.resources.ISaveParticipant#prepareToSave(org.eclipse.core.resources.ISaveContext)
	 */
	@Override
	public void prepareToSave(ISaveContext context) throws CoreException {
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.resources.ISaveParticipant#rollback(org.eclipse.core.resources.ISaveContext)
	 */
	@Override
	public void rollback(ISaveContext context) {
	}

}
